package ants;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Scanner;
import java.util.regex.Pattern;

import cell.Ant;
import cell.Cell;
import cell.Food;
import cell.Hill;
import cell.Land;
import cell.Visible;

import com.google.common.base.Function;
import com.google.common.base.Predicate;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Iterables;


/**
 * Holds all game data and current game state.
 */
public class GameState {
  
  /**
   * These are the constants set during the initial setup, i.e., before the
   * first turn. Their values are set by a call to setGameConstants( ) from
   * TurnManager.
   */
  
  private static enum SetupToken {
    ATTACKRADIUS2, COLS, LOADTIME, ROWS, SPAWNRADIUS2, TURNS, TURNTIME,
    VIEWRADIUS2;
    
    private static final Pattern PATTERN = compilePattern(SetupToken.values( ));
  }
  
  
  @SuppressWarnings("unused")
  private static int               attackRadius2;
  
  public static int                cols;
  
  @SuppressWarnings("unused")
  private static int               loadTime;
  
  public static int                rows;
  
  @SuppressWarnings("unused")
  private static int               spawnRadius2;
  
  /**
   * Throw an exception and finish the turn when there are fewer than this
   * number of milliseconds left.
   * 
   */
  private static final int         TURN_TIME_LOWER_BOUND = 10;
  
  @SuppressWarnings("unused")
  private static int               turns;
  
  public static int                turnTime;
  
  private static int               viewRadius2;
  
  public static ImmutableSet<Cell> visionOffsets;
  
  
  public static Pattern compilePattern(Enum<?>[] enumConstants) {
    final StringBuilder builder = new StringBuilder("(");
    for (final Enum<?> enumConstant: enumConstants) {
      builder.append(enumConstant.name( ));
      builder.append("|");
    }
    builder.deleteCharAt(builder.lastIndexOf("|"));
    builder.append(")");
    return Pattern.compile(builder.toString( ));
  }
  
  
  private static ImmutableSet<Cell> generateVisionOffsets( ) {
    final ImmutableSet.Builder<Cell> builder = ImmutableSet.builder( );
    final int mx = (int) Math.sqrt(viewRadius2);
    for (int row = -mx; row <= mx; ++row)
      for (int col = -mx; col <= mx; ++col) {
        final int d = row * row + col * col;
        if (d <= viewRadius2) builder.add(new Cell(row, col));
      }
    return builder.build( );
  }
  
  
  /**
   * Parses the setup information from system input stream.
   * 
   * @param input
   *          setup information
   */
  public static void setGameConstants(Iterable<String> input) {
    
    for (final String line: input) {
      final Scanner scanner = new Scanner(line);
      if (!scanner.hasNext( )) continue;
      
      final String token = scanner.next( ).toUpperCase( );
      if (!SetupToken.PATTERN.matcher(token).matches( )) continue;
      final SetupToken setupToken = SetupToken.valueOf(token);
      
      switch (setupToken) {
        case LOADTIME:
          loadTime = scanner.nextInt( );
          break;
        case TURNTIME:
          turnTime = scanner.nextInt( );
          break;
        case ROWS:
          rows = scanner.nextInt( );
          break;
        case COLS:
          cols = scanner.nextInt( );
          break;
        case TURNS:
          turns = scanner.nextInt( );
          break;
        case VIEWRADIUS2:
          viewRadius2 = scanner.nextInt( );
          break;
        case ATTACKRADIUS2:
          attackRadius2 = scanner.nextInt( );
          break;
        case SPAWNRADIUS2:
          spawnRadius2 = scanner.nextInt( );
          break;
      }
    }
    visionOffsets = generateVisionOffsets( );
  }
  
  
  public HOF.Function_2<CellProperty, Cell, Boolean> hasPropertyFunction = new HOF.Function_2<CellProperty, Cell, Boolean>( ) {
                                                                           @Override
                                                                           public Boolean apply2(CellProperty cellProperty,
                                                                                                 Cell cell) {
                                                                             return hasProperty(cellProperty, cell);
                                                                           }
                                                                         };
  
  public Predicate<Cell>                             isFoodPredicate     = hasPropertyFunction.curryToPredicate(CellProperty.FOOD);
  
  /** A directly defined alternative as a Function */
  public Function<Cell, Boolean>                     isLandFunction      = new Function<Cell, Boolean>( ) {
                                                                           @Override
                                                                           public Boolean apply(Cell cell) {
                                                                             return isVisible(cell) && !isWater(cell);
                                                                           }
                                                                         };
  
  public Predicate<Cell>                             isLandPredicate     = hasPropertyFunction.curryToPredicate(CellProperty.LAND);
  
  public Predicate<Cell>                             isMyAntPredicate    = hasPropertyFunction.curryToPredicate(CellProperty.MY_ANT);
  
  public Predicate<Cell>                             isWaterPredicate    = hasPropertyFunction.curryToPredicate(CellProperty.WATER);
  
  private final Properties                           properties;
  
  public final Map<Cell, BFS>                        searches            = new HashMap<>( );
  
  private final int                                  turnCount;
  
  private final long                                 turnStartTime       = System.currentTimeMillis( );
  
  
  
  public GameState( ) {
    turnCount = 0;
    Logger.gameState = this;
    properties = new Properties( );
  }
  
  
  public GameState(GameState prevGameState, List<String> turnInput) {
    // Must set turnCount and Logger.gameState before constructing the
    // Properties or log will have turn count wrong.
    turnCount = prevGameState.getTurnCount( ) + 1;
    Logger.gameState = this;
    properties = new Properties(prevGameState.properties, turnInput);
  }
  
  
  /**
   * Returns location with the specified offset from the specified location.
   * 
   * @param mapLoc
   *          location on the game map
   * @param offset
   *          offset to look up
   * @return location with <code>offset</code> from <cod>mapLoc</code>
   */
  public Cell applyOffset(Cell mapLoc, Cell offset) {
    int row = (mapLoc.row + offset.row) % rows;
    if (row < 0) row += rows;
    int col = (mapLoc.col + offset.col) % cols;
    if (col < 0) col += cols;
    return new Cell(row, col);
  }
  
  
  public void checkTime( ) throws TimeRunningOutException {
    checkTime(turnTime);
  }
  
  
  private void checkTime(int timeLimit) {
    final int timeRemaining = getTimeRemaining( );
    if (timeRemaining < TURN_TIME_LOWER_BOUND) throw new TimeRunningOutException("Time's up! (" + timeRemaining + " ms)");
  }
  
  
  /**
   * Return true or false depending on whether (a) from != to and (b) there is a
   * direct path from "from" to "to".
   * 
   * @param from
   * @param to
   * @return true or false depending on whether (a) from != to and (b) there is
   *         a direct path from "from" to "to".
   */
  public Direction directPathExists(Cell from, Cell to) {
    if (from.equals(to)) return null;
    final Direction direction = getNonWaterDirection(from, to);
    if (direction == null) return null;
    
    // We have a valid direction. Take the step to a neighbor. If we are there
    // or if the recursive call to directPathExists succeeds return the valid
    // direction. Otherwise return null.
    final Cell neighbor = direction.takeAStep(from);
    return neighbor.equals(to) ||
           isVisible(neighbor) && directPathExists(neighbor, to) != null ? direction
                                                                        : null;
  }
  
  public int getAttackRadius2(){
    return attackRadius2;
    
  }
  
  public ImmutableSet<CellProperty> getCellProperties(Cell cell) {
    final ImmutableSet.Builder<CellProperty> builder = ImmutableSet.builder( );
    for (final CellProperty cellProperty: CellProperty.values( ))
      if (hasProperty(cellProperty, cell)) builder.add(cellProperty);
    return builder.build( );
  }
  
  
  public ImmutableSet<CellProperty> getCellProperties2(final Cell cell) {
    final Predicate<CellProperty> cellHasPropertyPredicate = new Predicate<CellProperty>( ) {
      
      @Override
      public boolean apply(CellProperty cellProperty) {
        return hasProperty(cellProperty, cell);
      }
      
    };
    return ImmutableSet.copyOf(Iterables.filter(Arrays.asList(CellProperty.values( )), cellHasPropertyPredicate));
  }
  
  
  /**
   * 
   * @param from
   *          the "from" coordinate, either a row or a col. "from" and "to" are
   *          either both rows or both cols.
   * @param to
   *          the "to" coordinate, either a row or a col. "from" and "to" are
   *          either both rows or both cols.
   * @param max
   *          the max value of this axis, either rows or cols.
   * @param defaultDir
   *          the default direction if "from" and "to" are close and "from" <
   *          "to"
   * @return a list of Directions, either empty (if "to" == "from") or with one
   *         element indicating the shortest Direction from "from" to "to".
   */
  private List<Direction> getDir(int from, int to, int max, Direction defaultDir) {
    
    final List<Direction> dirs = new ArrayList<>( );
    if (from == to) return dirs;
    
    // Each condition
    // (i) Math.abs(to - from) > max / 2
    // (ii) from > to
    // requires the direction to be inverted. If both are true, invert twice,
    // i.e., do nothing. If neither is true also do nothing.
    dirs.add(Math.abs(to - from) > max / 2 != from > to ? defaultDir.invertDirection( )
                                                       : defaultDir);
    return dirs;
  }
  
  
  /**
   * Returns one or two orthogonal directions from one location to the another.
   * 
   * @param from
   *          one location on the game map
   * @param to
   *          another location on the game map
   * 
   * @return orthogonal directions from <code>from</code> to <code>to</code>
   */
  public List<Direction> getDirections(Cell from, Cell to) {
    final List<Direction> directions = new ArrayList<>( );
    
    directions.addAll(getDir(from.row, to.row, rows, Direction.SOUTH));
    directions.addAll(getDir(from.col, to.col, cols, Direction.EAST));
    
    return directions;
  }
  
  
  /**
   * Calculates distance between two locations on the game map.
   * 
   * @param ml_1
   *          one location on the game map
   * @param ml_2
   *          another location on the game map
   * 
   * @return distance between <code>ml_1</code> and <code>ml_2</code>
   */
  public int getDistance2(Cell ml_1, Cell ml_2) {
    int rowDelta = Math.abs(ml_1.row - ml_2.row);
    int colDelta = Math.abs(ml_1.col - ml_2.col);
    rowDelta = Math.min(rowDelta, rows - rowDelta);
    colDelta = Math.min(colDelta, cols - colDelta);
    return rowDelta * rowDelta + colDelta * colDelta;
  }
  
  
  public Iterable<? extends Cell> getEdgeCells( ) {
    return Iterables.filter(getVisibleCells( ), new Predicate<Cell>( ) {
      
      @Override
      public boolean apply(Cell cell) {
        return isLand(cell) && getVisibleNeighbors(cell).size( ) < 4;
      }
      
    });
  }
  
  
  public ImmutableSet<Hill> getMyHills( ) {
    return properties.getMyHills( );
  }

  
  public ImmutableSet<Hill> getEnemyHills( ) {
    return properties.getEnemyHills( );
  }
  
  
  public ImmutableSet<Food> getFood( ) {
    return properties.getFood( );
  }
  
  
  public ImmutableSet<Land> getLandCells( ) {
    return properties.getLand( );
  }
  
  
  public ImmutableList<Cell> getLandNeighbors(Cell cell) {
    return getNeighbors(cell, ImmutableSet.of(CellProperty.LAND), false);
  }
  
  
  public String getLogMessagePrefix( ) {
    return turnCount + ". (" + getTimeRemaining( ) + ")  ";
  }
  
  
  public ImmutableSet<Ant> getMyAnts( ) {
    return properties.getMyAnts( );
  }
  
  
  public Iterable<Cell> getNeighbors(Cell cell) {
    return Iterables.transform(Direction.getValuesAsList( ), cell.takeAStepFunction);
  }
  
  
  public ImmutableList<Cell> getNeighbors(Cell cell,
                                          Iterable<CellProperty> requiredProperties,
                                          boolean shuffle) {
    
    final Iterable<Cell> neighbors = getNeighbors(cell);
    
    final Predicate<Cell> filterPredicate = properties.hasPropertiesFunction.curryToPredicate(requiredProperties);
    
    final ImmutableList<Cell> filteredNeighbors = ImmutableList.copyOf(Iterables.filter(neighbors, filterPredicate));
    
    return shuffle ? Utils.shuffle(filteredNeighbors) : filteredNeighbors;
  }
  
  
  /**
   * Look at the at-most two possible neighbors from "from" toward "to". Select
   * (arbitrarily) the first one that is a valid move. This could be extended to
   * a depth-first search. It wouldn't be a good idea since too much overlap
   * would be generated.
   * 
   * @param from
   * @param to
   * @return
   */
  public Direction getNonWaterDirection(final Cell from, Cell to) {
    
    final ImmutableList<Direction> directions = ImmutableList.copyOf(Utils.shuffle(Direction.getDirections(from, to)));
    
    for (final Object direction: directions) {
      final Direction dir = (Direction) direction;
      if (!isWater(dir.takeAStep(from))) return dir;
    }
    return null;
  }
  
  
  public Cell getPrevious(Cell cell) {
    return properties.getPrevious(new Cell(cell));
  }
  
  
  public ImmutableList<Cell> getShuffledLandNeighbors(Cell cell) {
    return getNeighbors(cell, ImmutableSet.of(CellProperty.LAND), true);
  }
  
  
  /**
   * Returns the number of milliseconds remaining before timing out.
   * 
   * @return the number of milliseconds remaining before timing out.
   */
  private int getTimeRemaining( ) {
    return GameState.turnTime - (int) (System.currentTimeMillis( ) - turnStartTime) - 1;
  }
  
  
  public int getTurnCount( ) {
    return turnCount;
  }
  
  
  public int getTurnsSinceLastSeen(Cell mapLoc) {
    return properties.getTurnsSinceLastSeen(mapLoc, turnCount);
  }
  
  
  public ImmutableSet<Visible> getVisibleCells( ) {
    return properties.getVisibleCells( );
  }
  
  
  private ImmutableList<Cell> getVisibleNeighbors(Cell visibleCell) {
    return getNeighbors(visibleCell, ImmutableSet.of(CellProperty.VISIBLE), false);
  }
  
  
  public boolean hasProperty(CellProperty cellProperty, Cell cell) {
    return properties.hasProperty(cellProperty, cell);
  }
  
  
  public boolean isATrap(Cell cell) {
    return getLandNeighbors(cell).size( ) == 1 & !(hasProperty(CellProperty.ENEMY_HILL, cell));
  }
  
  
  public boolean isFood(Cell cell) {
    return hasProperty(CellProperty.FOOD, cell);
  }
  
  
  public boolean isLand(Cell cell) {
    return hasProperty(CellProperty.LAND, cell);
  }
  
  
  public boolean isMyAnt(Cell cell) {
    return hasProperty(CellProperty.MY_ANT, cell);
  }
  
  
  
  public void issueOrder(Cell myAnt, Cell destination) {
    if (destination.equals(myAnt) || getMyHills().contains(destination)) return;
    if(getMyAnts().contains(destination)) return;
    final String order = "o " + myAnt.row + " " + myAnt.col + " " +
                         Direction.getDirections(myAnt, destination).get(0);
    print(order);
    properties.putPrevious(new Cell(destination), myAnt);
  }
  
  
  public boolean isVisible(Cell cell) {
    return properties.getVisibleCells( ).contains(cell);
  }
  
  
  public boolean isWater(Cell cell) {
    return hasProperty(CellProperty.WATER, cell);
  }
  
  
  public void print(String string) {
    System.out.println(string);
    //Logger.log("<- " + string);
    if (string.equals(TurnManager.GO)) System.out.flush( );
  }
  
  
  @Override
  public String toString( ) {
    return properties.toString( );
  }


  public ImmutableSet<Ant> getEnemyAnts( ) {
      return properties.getEnemyAnts( );

  }
  
  
}
